基于原型(prototype)的编程其实也是面向对象编程的一种方式。没有 class 化的,直接使用对象。又叫,基于实例的编程。其主流的语言就是 JavaScript。与传统的面对象编程的比较如下。
因为如此,很多基于原型的系统提倡运行时原型的修改,而只有极少数基于类的面向对象系统(比如第一个动态面向对象的系统 Smalltalk)允许类在程序运行时被修改。
这里,我们主要以 JavaScript 举例,面向对象里面要有个 Class。但是 JavaScript 觉得不是这样的,它就是要基于原型编程,就不要 Class,就直接在对象上改就行了。基于编程的修改,直接对类型进行修改。
我们先来看一个示例。
var foo = {name: "foo", one: 1, two: 2};
var bar = {three: 3};
每个对象都有一个 __proto__
的属性,这个就是“原型”。对于上面的两个对象,如果我们把
foo
赋值给 bar.__proto__
,那就意味着,bar
的原型就成了
foo
的。
bar.__proto__ = foo; // foo is now the prototype of bar.
于是,我们就可以在 bar
里面访问 foo
的属性了。
// If we try to access foo's properties from bar
// from now on, we'll succeed.
bar.one // Resolves to 1.
// The child object's properties are also accessible.
bar.three // Resolves to 3.
// Own properties shadow prototype properties
bar.name = "bar";
foo.name; // unaffected, resolves to "foo"
bar.name; // Resolves to "bar"
需要解释一下 JavaScript 的两个东西,一个是 __proto__
,另一个是
prototype
,这两个东西很容易混淆。这里说明一下:
new
来创建一个对象时构造 __proto__
用的。它是构造函数的一个属性。
在 JavaScript 中,对象有两种表现形式, 一种是 Object
(ES5 关于 Object
的文档),一种是 Function
(ES5 关于 Function
的文档)。
我们可以简单地认为,__proto__
是所有对象用于链接原型的一个指针,而 prototype
则是 Function 对象的属性,其主要是用来当需要 new 一个对象时让 __proto__
指针所指向的地方。 对于超级对象 Function
而言,
Function.__proto__
就是 Function.prototype
。
比如我们有如下的代码:
var a = {
x: 10,
calculate: function (z) {
return this.x + this.y + z;
}
};
var b = {
y: 20,
__proto__: a
};
var c = {
y: 30,
__proto__: a
};
// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80
其中的“原型链”如下所示:
注意:ES5 中,规定原型继承需要使用 Object.create()
函数。如下所示:
var b = Object.create(a, {y: {value: 20}});
var c = Object.create(a, {y: {value: 30}});
好了,我们再来看一段代码:
// 一种构造函数写法
function Foo(y) {
this.y = y;
}
// 修改 Foo 的 prototype,加入一个成员变量 x
Foo.prototype.x = 10;
// 修改 Foo 的 prototype,加入一个成员函数 calculate
Foo.prototype.calculate = function (z) {
return this.x + this.y + z;
};
// 现在,我们用 Foo 这个原型来创建 b 和 c
var b = new Foo(20);
var c = new Foo(30);
// 调用原型中的方法,可以得到正确的值
b.calculate(30); // 60
c.calculate(40); // 80
那么,在内存中的布局是怎么样的呢?大概是下面这个样子。
这个图应该可以让你很好地看明白 __proto__
和 prototype
的差别了。
我们可以测试一下:
b.__proto__ === Foo.prototype, // true
c.__proto__ === Foo.prototype, // true
b.constructor === Foo, // true
c.constructor === Foo, // true
Foo.prototype.constructor === Foo, // true
b.calculate === b.__proto__.calculate, // true
b.__proto__.calculate === Foo.prototype.calculate // true
这里需要说明的是——
Foo.prototype 自动创建了一个属性 constructor,这是一个指向函数自己的一个 reference。这样一来,对于实例 b 或 c 来说,就能访问到这个继承的 constructor 了。
有了这些基本概念,我们就可以讲一下 JavaScript 的面向对象编程了。
注: 上面示例和图示来源于 JavaScript, The Core 一文。
我们再来重温一下上面讲述的内容:
function Person(){}
var p = new Person();
Person.prototype.name = "Hao Chen";
Person.prototype.sayHello = function(){
console.log("Hi, I am " + this.name);
}
console.log(p.name); // "Hao Chen"
p.sayHello(); // "Hi, I am Hao Chen"
在上面这个例子中:
Person()
;new
出另一个对象,存在 p
中;Person.prototype
,让其有一个 name
的属性和一个
sayHello()
的方法;
p
的对象也跟着一起改变了。注意一下,
function Person(){}
时,Person.__proto__
指向 Function.prototype
;
var p = new Person()
时,p.__proto__
指向 Person.prototype
;
Person.prototype
的内容后,p.__proto__
的内容也就被改变了。
好了,我们再来看一下“原型编程”中面向对象的编程玩法。
首先,我们定义一个 Person
类。
//Define human class
var Person = function (fullName, email) {
this.fullName = fullName;
this.email = email;
this.speak = function(){
console.log("I speak English!");
};
this.introduction = function(){
console.log("Hi, I am " + this.fullName);
};
}
上面这个对象中,包含了:
fullName
和 email
;speak()
和 introduction()
。其实,所谓的方法也是属性。
然后,我们可以定义一个 Student
对象。
//Define Student class
var Student = function(fullName, email, school, courses) {
Person.call(this, fullName, email);
// Initialize our Student properties
this.school = school;
this.courses = courses;
// override the "introduction" method
this.introduction= function(){
console.log("Hi, I am " + this.fullName +
". I am a student of " + this.school +
", I study "+ this.courses +".");
};
// Add a "exams" method
this.takeExams = function(){
console.log("This is my exams time!");
};
};
在上面的代码中:
Person.call(this, fullName, email)
,call()
或 apply()
都是为了动态改变 this
所指向的对象的内容而出现的。这里的
this
就是 Student
。
introduction()
方法,并新增加了一个 takeExams()
的方法。
虽然,我们这样定义了 Student
,但是它还没有和 Person
发生继承关系。为了要让它们发生关系,我们就需要修改 Student
的原型。
我们可以简单粗暴地做赋值:Student.__proto__ = Person.prototype
,但是,这太粗暴了。
我们还是使用比较规范的方式:
Object.create()
来将Person.prototype
和 Student.prototype
关联上。
Student.prototype.constructor = Student;
。// Create a Student.prototype object that inherits
// from Person.prototype.
Student.prototype = Object.create(Person.prototype);
// Set the "constructor" property to refer to Student
Student.prototype.constructor = Student;
这样,我们就可以这样使用了。
var student = new Student("Hao Chen",
"[email protected]",
"XYZ University",
"Computer Science");
student.introduction();
student.speak();
student.takeExams();
// Check that instanceof works correctly
console.log(student instanceof Person); // true
console.log(student instanceof Student); // true
上述就是基于原型的面向对象编程的玩法了。
注:在 ECMAScript 标准的第四版开始寻求使 JavaScript 提供基于类的构造,且 ECMAScript 第六版有提供 "class"(类) 作为原有的原型架构之上的语法糖,提供构建对象与处理继承时的另一种语法。
我们可以看到,这种玩法就是一种委托的方式。在使用委托的基于原型的语言中,运行时语言可以仅仅通过循着一个序列的指针直到找到匹配这样的方式来定位属性或者寻找正确的数据。所有这些创建行为共享的行为需要的是委托指针。
不像是基于类的面向对象语言中类和接口的关系,原型和它的分支之间的关系并不要求子对象有相似的内存结构,因为如此,子对象可以继续修改而无需像基于类的系统那样整理结构。还有一个要提到的地方是,不仅仅是数据,方法也能被修改。因为这个原因,大多数基于原型的语言把数据和方法提作“slots”。
这种在对象里面直接修改的玩法,虽然这个特性可以带来运行时的灵活性,我们可以在运行时修改一个 prototype,给它增加甚至删除属性和方法。但是其带来了执行的不确定性,也有安全性的问题,而代码还变得不可预测,这有点黑科技的味道了。因为这些不像静态类型系统,没有一个不可变的契约对代码的确定性有保证,所以,需要使用者来自己保证。